#!/usr/bin/env python3
"""Deduplicate attachments by content hash and update HTML references.

Usage:
    python dedupe_att.py --att <att_dir> --html <html_path>

行为说明（中文）:
 - 对 `att_dir` 下的每个文件计算 SHA256（非递归）。
 - 将内容相同（哈希一致）的文件分为一组。
 - 对每组选择一个保留文件：
     * 优先选择常见的图片/视频/音频扩展（例如 .jpg/.png/.mp4/.mp3）
     * 否则按文件名长度/字典序选择第一个作为保留
 - 删除组内其它文件（静默删除），并记录替换映射
 - 更新指定的 HTML 文件：把删除的文件名替换为保留文件名，以保持引用一致
"""
import argparse
import hashlib
from pathlib import Path
from collections import defaultdict

COMMON_EXT_PREF = ['.jpg', '.jpeg', '.png', '.gif', '.mp4', '.mp3', '.wav']

def sha256(path: Path):
    h = hashlib.sha256()
    with path.open('rb') as f:
        for chunk in iter(lambda: f.read(8192), b''):
            h.update(chunk)
    return h.hexdigest()


def _debug_print(msg: str):
    # 内部调试打印的统一入口；当前保持静默或可修改为打印
    # 这里暂不打印，以避免在大量文件时输出噪声；如需调试可改为 print(msg)
    return

def choose_representative(files):
    # files: list of Path
    # prefer common extensions
    for ext in COMMON_EXT_PREF:
        for p in files:
            if p.suffix.lower() == ext:
                return p
    # otherwise prefer shortest name then alphabetical
    files_sorted = sorted(files, key=lambda p: (len(p.name), p.name))
    return files_sorted[0]

def main():
    p = argparse.ArgumentParser()
    p.add_argument('--att', required=True, help='attachments directory')
    p.add_argument('--html', required=True, help='html file to update')
    args = p.parse_args()

    att = Path(args.att)
    html = Path(args.html)
    if not att.exists() or not att.is_dir():
        print('attachments dir not found:', att)
        return
    if not html.exists():
        print('html file not found:', html)
        return

    # 列出目录下的文件（非递归），并打印数量以便用户了解规模
    files = [p for p in att.iterdir() if p.is_file()]
    print('Found', len(files), 'files in', att)

    # 按内容哈希分组
    groups = defaultdict(list)
    for f in files:
        try:
            h = sha256(f)
        except Exception as e:
            # 若读取或哈希失败则跳过该文件
            print('skip', f, 'error', e)
            continue
        groups[h].append(f)

    # mapping of old -> new
    replace_map = {}
    removed = []
    for h, plist in groups.items():
        if len(plist) <= 1:
            continue
        rep = choose_representative(plist)
        for p in plist:
            if p == rep:
                continue
            replace_map[p.name] = rep.name
            try:
                p.unlink()
                removed.append(p.name)
            except Exception as e:
                print('failed remove', p, e)

    # 更新 HTML：把所有被删除的文件名替换为保留的文件名
    if replace_map:
        txt = html.read_text(encoding='utf-8', errors='replace')
        for old, new in replace_map.items():
            txt = txt.replace(old, new)
        html.write_text(txt, encoding='utf-8')

    print('Removed files:', len(removed))
    if removed:
        for r in removed[:50]:
            print('  ', r)
    print('Updated', html)

if __name__ == '__main__':
    main()
